<!--
@description: 周选择日历
@author: ldb
@time: 2022-09-26 21:34:28
-->
<template>
  <div class="week-calendar">
    <!--切换栏-->
    <div class="calendar-header">
      <slot name="header" :data="{ year, month, week, value }">
        <a-select v-model:value="year" :options="yearOptions" @change="handleChange($event, 1)">
          <template #suffixIcon><caret-down-outlined /></template>
        </a-select>
        <a-select v-model:value="month" :options="monthOptions" @change="handleChange($event, 2)">
          <template #suffixIcon><caret-down-outlined /></template>
        </a-select>
        <a-select v-model:value="week" :options="weekOptions" @change="handleChange($event, 3)">
          <template #suffixIcon><caret-down-outlined /></template>
        </a-select>
      </slot>
    </div>
    <table class="calendar-table">
      <!--表头-->
      <tr>
        <th v-if="columns.length > 0">时间</th>
        <th v-for="(item, index) in header" :key="index" :class="[isToday(index + 1) ? 'today' : '']">
          {{ item }}
        </th>
      </tr>
      <!--表体-->
      <tr v-for="(item, index) in columns" :key="`${item.startTime}-${item.stopTime}`">
        <td
          :class="{ 'td-bg': (index + 1) % 2 == 0 }"
          :style="{
            borderRight: border == true ? borderStyle.hasBorder : borderStyle.unHasBorder,
          }"
        >
          <slot name="column" :data="item">
            <div class="column-time">
              <div>{{ item.timeQuantum + item.startTime }}</div>
              <div class="line"></div>
              <div>{{ item.timeQuantum + item.stopTime }}</div>
            </div>
          </slot>
        </td>
        <!--每个时间段的时间-->
        <td
          v-for="(week, index2) in showData[index]"
          :key="'week' + index2"
          :class="{ 'td-bg': (index + 1) % 2 == 0 }"
          :style="{
            borderRight: border == true ? borderStyle.hasBorder : borderStyle.unHasBorder,
          }"
          :title="DateToStr(week.date)"
          @click="handlerClickDay(DateToStr(week.date), week)"
        >
          <div class="current-day">
            <slot name="day" :value="week.matchDay">
              <div v-show="week.matchDay?.length > 0" class="content">
                <div class="num">{{ week.matchDay?.length }}</div>
                <div
                  v-for="(item, index) in week.matchDay"
                  :key="index"
                  class="item"
                  :style="getMatchStyle(item)"
                  @click="$emit('selectChange', item)"
                >
                  {{ DateToStr(item.time, 'YYYY-MM-DD HH:mm:ss') || '暂无' }}
                </div>
              </div>
            </slot>
          </div>
        </td>
      </tr>
    </table>
  </div>
</template>

<script lang="ts" setup>
import { ref, onMounted, computed, defineEmits, defineProps, withDefaults, watch, nextTick } from 'vue'
import { CaretDownOutlined } from '@ant-design/icons-vue'
import dayjs, { Dayjs } from 'dayjs'
import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn')
import { DateToStr, getMonthWeeksByYears, StrToDayjs } from '@/components/week-calendar/week-calendar-util'
import { BaseOptions } from '@/components/base/form-item/props'
import getRandomColor from '@/utils/color'
// 周选择器组件属性
interface WeekCalendarProps {
  // 是否需要边框
  border?: boolean
  // 头部星期样式
  header?: Array<string>
  // 匹配的日期
  matchDays?: Array<MatchDay>
  // 周选择器列数据
  columns?: Array<WeekCalendarColumns>
}
/**
 * 周选择器时间列数据
 */
export interface WeekCalendarColumns {
  // 时间段
  timeQuantum: 'AM' | 'PM' | '上午' | '下午' | '晚上' | '凌晨'
  // 开始时间
  startTime: string
  // 结束时间
  stopTime: string
}
// 组件内数据
export interface WeekCalendarValue {
  // 当前日期(实际日期)
  readonly date: Dayjs
  // 当天日期数据
  data?: any
  // 匹配时间
  matchDay?: Array<MatchDay>
}
// 匹配时间
export interface MatchDay {
  // 具体时间
  time: Dayjs
  // 背景颜色
  bgColor?: string
  // 文字颜色
  color?: string
  // 传入的值
  data?: any
}
// 周范围属性
export interface WeekRangeProps {
  // 开始时间
  startDate: Dayjs
  // 结束时间
  endDate: Dayjs
}
// 边框样式
const borderStyle = {
  hasBorder: '1px solid rgba(0, 0, 0, 0.1)',
  unHasBorder: '0px solid rgba(0, 0, 0, 0.1)',
}
// 获取匹配项样式
function getMatchStyle(data: MatchDay) {
  if (!data.color) {
    data.color = getRandomColor()
  }
  if (!data.bgColor) {
    data.bgColor = getRandomColor()
  }
  return { backgroundColor: data.bgColor, color: data.color }
}
// 属性
const props = withDefaults(defineProps<WeekCalendarProps>(), {
  border: true,
  header: () => ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期天'],
  matchDays: () => [],
  columns: () => [],
})
// 事件
const emits = defineEmits<{
  // 点击某一天触发
  (name: 'change', value: WeekCalendarValue): void
  // 点击某一天匹配的某一条数据触发
  (name: 'selectChange', value: MatchDay): void
  // 当周数改变时触发(返回这周第一天和最后一天)
  (name: 'weekChange', value: WeekRangeProps): void
}>()
// 日历本身数据
export type CalendarValues = Array<Array<WeekCalendarValue>>
// 用户当前选择的时间
const value = ref<String>(DateToStr(dayjs()))
// 本月所有周数据
const currentWeek = ref<CalendarValues>([])
// 最终展示的数据
const showData = ref<CalendarValues>([])
// 年、月、周数据
const year = ref<number>()
const month = ref<string>()
const week = ref<number>()
const yearOptions = ref<Array<BaseOptions>>([])
const monthOptions = ref<Array<BaseOptions>>([])
const weekOptions = ref<Array<BaseOptions>>([])
// 获取月
const getMonths = (value: Dayjs = dayjs()) => {
  // @ts-ignore
  const localeData = value.localeData()
  const months = []
  for (let i = 0; i < 12; i++) {
    months.push({
      label: localeData.monthsShort(value.month(i)),
      value: localeData.monthsShort(value.month(i)).substring(0, localeData.monthsShort(value.month(i)).length - 1),
    })
  }
  return months
}
// 获取年
const getYears = (value: Dayjs = dayjs()) => {
  const year = value.year()
  const years = []
  for (let i = year - 10; i < year + 10; i += 1) {
    years.push({
      label: i + '年',
      value: i + '',
    })
  }
  return years
}
// 更新周时间
function updateWeekByDate(value: Dayjs = dayjs()) {
  currentWeek.value = getMonthWeeksByYears(value).value
}
// 获取周
function getWeeks(value: Dayjs = dayjs()) {
  updateWeekByDate(value)
  if (currentWeek.value.length > 0) {
    const curWeek = week.value
    const time = currentWeek.value[curWeek as number]
    if (time) {
      emits('weekChange', {
        startDate: time[0].date,
        endDate: time[time.length - 1].date,
      })
    }
  }
  const weeks = []
  const weekArr = ['', '一', '二', '三', '四', '五', '六']
  for (let i = 1; i <= currentWeek.value.length; i++) {
    weeks.push({
      label: '第' + weekArr[i] + '周',
      value: i - 1,
    })
  }
  return weeks as unknown as Array<BaseOptions>
}
onMounted(() => {
  // 获取总周数
  updateWeekByDate()
  // 获取当前周
  getCurrentWeek()
  // 更新数据
  updateDate()
})
// 当前年月日数组
const time = computed(() => value.value.split('-'))
/**
 * 获取当前周
 */
function getCurrentWeek(time = dayjs()) {
  // 循环这个月的每一周，判断当前时间在哪个区间内
  let flag = true
  for (let i = 0; i < currentWeek.value.length; i++) {
    const startDate = currentWeek.value[i][0].date
    const stopDate = currentWeek.value[i][currentWeek.value[i].length - 1].date
    if (
      time.isSame(startDate, 'day') ||
      time.isSame(stopDate, 'day') ||
      (time.isBefore(stopDate, 'day') && time.isAfter(startDate, 'day'))
    ) {
      week.value = i
      flag = false
      break
    }
  }
  // todo 有可能今天并不在当月内
  // 不匹配默认为第一周
  if (flag) {
    week.value = 0
  }
}
/**
 * 年月周切换
 * @param val 当前选择的数据
 * @param type 1=年 2=月 3=周
 */
function handleChange(val: number, type?: number) {
  // 先获取当前时间
  let time: Dayjs & { [key: string]: any } = StrToDayjs(value.value as string)
  switch (type) {
    case 1:
      time = time.year(val)
      week.value = 0
      break
    case 2:
      time = time.month(val - 1)
      week.value = 0
      break
  }
  // 传递给父组件一个开始时间段和结束时间段，调用接口更新数据
  updateDate(time)
}
/**
 * 日期数据初始化
 */
function updateDate(now: Dayjs = dayjs()) {
  value.value = DateToStr(now, 'YYYY-MM-DD')
  // 年月周更新
  yearOptions.value = getYears(now)
  monthOptions.value = getMonths(now)
  weekOptions.value = getWeeks(now)
  year.value = time.value[0] as unknown as number
  month.value = parseInt(time.value[1]) + ''
  // 更新匹配时间
  updateMatchData()
}
/**
 * 更新匹配时间(只匹配本周的数据)
 */
async function updateMatchData() {
  // todo 用时间段和这周的每一天相乘匹配，获取最终的数据
  // 1、先获取本周时间
  const nowWeek = currentWeek.value[week.value as number]
  // 2、循环遍历每个时间段，和每周时间进行匹配,如果当前时间段-星期-匹配时间相吻合，则加入的最终的集合中
  let resList: CalendarValues = []
  if (nowWeek && nowWeek.length > 0) {
    await props.columns.forEach((timeRange) => {
      resList.push(
        nowWeek.map((day) => {
          // 3、根据这两个条件，加上需要匹配的数据，获取符合条件的数据
          return getMatchData(timeRange, day)
        }),
      )
    })
  }
  showData.value = resList
}

/**
 * 获取当前时间段匹配的时间数据
 * @param timeRange 当前时间段
 * @param day 当前时间
 */
function getMatchData(timeRange: WeekCalendarColumns, day: WeekCalendarValue): WeekCalendarValue {
  // 1、解构数据
  let res: WeekCalendarValue = {} as WeekCalendarValue
  Object.assign(res, day)
  // 2、获取当天的开始时间和结束时间,遍历匹配的数据是否在此区间内
  const startTimeArr = timeRange.startTime.split(':')
  const stopTimeArr = timeRange.stopTime.split(':')
  // 异常处理
  if (startTimeArr.length != 2 || stopTimeArr.length != 2) {
    throw new Error(
      `开始或结束时间格式有误，正确格式为 09:30。当前开始时间为${timeRange.startTime},结束时间为${timeRange.stopTime}`,
    )
  }
  // 获取开始结束时间段日期
  const startDate = day.date.hour(parseInt(startTimeArr[0])).minute(parseInt(startTimeArr[1]))
  const stopDate = day.date.hour(parseInt(stopTimeArr[0])).minute(parseInt(stopTimeArr[1]))
  // 数据正常过滤
  res.matchDay = props.matchDays.filter((val) => {
    // 获取当天时间
    const time = val.time
    // 匹配时间在开始时间和结束时间之间
    return (
      time.isSame(startDate, 'minute') ||
      time.isSame(stopDate, 'minute') ||
      (time.isBefore(stopDate, 'minute') && time.isAfter(startDate, 'minute'))
    )
  })
  return res
}

/**
 * 点击某一天
 * @param val 日期值
 * @param data 数据
 */
function handlerClickDay(val: string, data: WeekCalendarValue) {
  // 交给父组件处理
  emits('change', data)
}

/**
 * 判断当前星期是否是今天
 * @param day 星期几
 */
function isToday(day: number) {
  // todo 判断今天是否在当前周，并且当前星期等于今天
  let nowWeek = currentWeek.value[week.value as number]
  if (nowWeek) {
    let startDate = nowWeek[0].date
    let stopDate = nowWeek[nowWeek.length - 1].date
    const time = dayjs()
    if (
      time.isSame(startDate, 'day') ||
      time.isSame(stopDate, 'day') ||
      (time.isBefore(stopDate, 'day') && time.isAfter(startDate, 'day'))
    ) {
      return day == time.day()
    }
  }
  return false
}

watch(props, () => {
  updateMatchData()
})
</script>

<style scoped lang="scss">
@import 'style/week-calendar';
</style>
